/**
 * Общие методы и свойства для rocon
 * @author Sergey Chikuyonok (sc@design.ru)
 * @copyright Art.Lebedev Studio (http://www.artlebedev.ru)
 */

var re_rule = /\.rc(\d+)\b/,
	re_class = /\brc(\d+)\b/,
	re_shape_flag = /\brc-shape\b/,

	/** Префиск для создаваемых CSS-правил */
	rule_prefix = 'rocon__',

	/** Базовый класс для создаваемых элементов */
	base_class = 'rocon',
	
	/** Привязанные к определенным классам фоны */
	binded_props = [],

	/** Результат, возвращаемый в объект <code>rocon</code> */
	result = {
		/**
		 * Добавление/обновление уголков для динамически созданных элементов.
		 * Может принимать неограниченное количество элементов либо массивов
		 * элементов, у которых нужно обновить уголки  
		 */
		update: function(){},
		bindProperties: function(){
			var id = 1;
			return function(rule, bg, border_width) {
				binded_props.push({
					'id': id++,
					'rule': rule, 
					'bg': mapArray(expandProperty(bg), function(val){
						if (val.charAt(0) != '#')
							val = '#' + val;
						return convertColorToHex(val);
					}),
					'border_width': border_width || 0
				});
			}
		}(),
		
		process: function(context) {
			processRoundedElements(context);
		}
	},

	/** @type {CSSStyleSheet} Таблица стилей для уголков */
	corners_ss = null,

	/** Кэш для уголков */
	_corner_cache = {},
	
	/** Классы элементов, которым нужно добавить скругленные уголки */
	elem_classes = [],
	
	/** Список функций, которые нужно выполнить при загрузке DOM-дерева */
	dom_ready_list = [],
	
	/** Загрузился ли DOM? */
	is_ready = false,
	
	/** Привязано ли событие, ожидающее загрузку DOM? */
	readyBound = false,

	userAgent = navigator.userAgent.toLowerCase(),
	
	/** 
	 * CSS-селекторы, которые уже были добавлены в стили. 
	 * Используется для того, чтобы не создавать одинаковые правила
	 */
	processed_rules = {},

	/** Тип и версия браузера пользователя. Взято с jQuery */
	browser = {
		version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [])[1],
		safari: /webkit/.test( userAgent ),
		opera: /opera/.test( userAgent ),
		msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
		mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
	};

/**
 * Выполняет все функции, добавленные на событие onDomContentLoaded.
 * Взято с jQuery
 */
function fireReady() {
	//Make sure that the DOM is not already loaded
	if (!is_ready) {
		// Remember that the DOM is ready
		is_ready = true;

		// If there are functions bound, to execute
		if ( dom_ready_list.length ) {
			
			for (var i = 0; i < dom_ready_list.length; i++) {
				dom_ready_list[i].call(document);
			}
			
//			walkArray(dom_ready_list, function(){
//				this.call(document);
//			});

			// Reset the list of functions
			dom_ready_list = null;
		}
	}
}

/**
 * Добавляет слушателя на событие onDomContentLoaded
 * @type {Function} fn Слушатель
 */
function addDomReady(fn) {
	dom_ready_list.push(fn);
}

/**
 * Проверка на наступление события onDomContentLoaded. 
 * Взято с jQuery
 */
function bindReady(){
	if ( readyBound ) return;
	readyBound = true;

	// Mozilla, Opera and webkit nightlies currently support this event
	if ( document.addEventListener ) {
		// Use the handy event callback
		document.addEventListener( "DOMContentLoaded", function(){
			document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
			fireReady();
		}, false );

	// If IE event model is used
	} else if ( document.attachEvent ) {
		// ensure firing before onload,
		// maybe late but safe also for iframes
		document.attachEvent("onreadystatechange", function(){
			if ( document.readyState === "complete" ) {
				document.detachEvent( "onreadystatechange", arguments.callee );
				fireReady();
			}
		});

		// If IE and not an iframe
		// continually check to see if the document is ready
		if ( document.documentElement.doScroll && !window.frameElement ) (function(){
			if ( is_ready ) return;

			try {
				// If IE is used, use the trick by Diego Perini
				// http://javascript.nwbox.com/IEContentLoaded/
				document.documentElement.doScroll("left");
			} catch( error ) {
				setTimeout( arguments.callee, 0 );
				return;
			}

			// and execute any waiting functions
			fireReady();
		})();
	}
}

/**
 * Вспомогательная функция, которая пробегается по всем элементам массива
 * <code>ar</code> и выполняет на каждом элементе его элементе функцию
 * <code>fn</code>. <code>this</code> внутри этой функции указывает на 
 * элемент массива
 * @param {Array} ar Массив, по которому нужно пробежаться
 * @param {Function} fn Функция, которую нужно выполнить на каждом элементе массива
 * @param {Boolean} forward Перебирать значения от начала массива (п умолчанию: с конца)
 */
function walkArray(ar, fn, forward) {
	if (forward) {
		for (var i = 0, len = ar.length; i < len; i++)
			if (fn.call(ar[i], i) === false)
				break;
	} else {
		for (var i = ar.length - 1, result; i >= 0; i--)
			if (fn.call(ar[i], i) === false)
				break;
	}
}

/**
 * Преобразует один массив элементов в другой с помощью функции callback.
 * Взято в jQuery
 * @param {Array} elems
 * @param {Function} callback
 * @return {Array}
 */
function mapArray(elems, callback) {
	var ret = [];

	// Go through the array, translating each of the items to their
	// new value (or values).
	for ( var i = 0, length = elems.length; i < length; i++ ) {
		var value = callback( elems[ i ], i );

		if ( value != null )
			ret[ ret.length ] = value;
	}

	return ret.concat.apply( [], ret );
}

/**
 * Функция добавления скругленных уголков элементу. Для каждого браузера 
 * будет своя функция
 */
function addCorners(){
	return;
};

// TODO Добавить исключение при правильной работе border-radius

/**
 * Преобразует цвет из RGB-предствления в hex
 * @param {String} color
 * @return {String}
 */
function convertColorToHex(color) {
	var result;
	function s(num) {
		var n = parseInt(num, 10).toString(16);
		return (n.length == 1) ? n + n : n;
	}
	
	function p(num) {
		return s(Math.round(num * 2.55));
	}
	
	if (result = /rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/.exec(color))
		return '#' + s(result[1]) + s(result[2]) + s(result[3]);

	// Look for rgb(num%,num%,num%)
	if (result = /rgb\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*\)/.exec(color))
		return '#' + p(result[1]) + p(result[2]) + p(result[3]); 

	// Look for #a0b1c2
	if (result = /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/i.exec(color))
		return '#' + result[1] + result[2] + result[3];
		
	if (result = /#([a-f0-9])([a-f0-9])([a-f0-9])/i.exec(color))
		return '#' + result[1] + result[1] + result[2] + result[2] + result[3] + result[3];
	
	s = null;
	p = null;
		
	return color;
}

/**
 * Создает HTML-элемент <code>name</code> с классом <code>class_name</code>
 * @param {String} name Название элемента
 * @param {String} class_name Класс элемента
 * @return {Element}
 */
function createElement(name, class_name) {
	var elem = document.createElement(name);
	if (class_name) {
		elem.className = class_name;
	}
	return elem;
}

/**
 * Простая проверка наличия определенного класса у элемента
 * @param {HTMLElement} elem
 * @param {String} class_name
 * @return {Boolean}
 */
function hasClass(elem, class_name) {
	var re = new RegExp('\\b' + class_name + '\\b');
	return elem.nodeType == 1 && re.test(elem.className || '');
}

/**
 * Возвращает значение CSS-свойства <b>name</b> элемента <b>elem</b>
 * @author John Resig (http://ejohn.org)
 * @param {Element} elem Элемент, у которого нужно получить значение CSS-свойства
 * @param {String|Array} name Название CSS-свойства
 * @return {String|Object}
 */
function getStyle(elem, name) {
	var cs, 
		result = {},
		camel = function(str, p1){return p1.toUpperCase();};
	
	walkArray(name instanceof Array ? name : [name], function(){
		var n = this, 
			name_camel = n.replace(/\-(\w)/g, camel);
		
		// If the property exists in style[], then it's been set
		// recently (and is current)
		if (elem.style[name_camel]) {
			result[name_camel] = elem.style[name_camel];
		}
		//Otherwise, try to use IE's method
		else if (browser.msie) {
			result[name_camel] = elem.currentStyle[name_camel];
		}
		// Or the W3C's method, if it exists
		else if (document.defaultView && document.defaultView.getComputedStyle) {
			if (!cs)
				cs = document.defaultView.getComputedStyle(elem, "");
			result[name_camel] = cs && cs.getPropertyValue(n);
		}
	});
	
	return name instanceof Array ? result : result[name.replace(/\-(\w)/g, camel)];
		
}

/**
 * Разворачивает краткую запись четырехзначного свойства в полную:<br>
 * 	— a      -&gt; a,a,a,a<br>
 *	— a_b    -&gt; a,b,a,b<br>
 *	— a_b_с  -&gt; a,b,с,b<br>
 * 
 * @param {String} prop Значение, которое нужно раскрыть
 * @return {Array} Массив с 4 значениями
 */
function expandProperty(prop) {
	var chunks = (prop || '').split('_');
		
	switch (chunks.length) {
		case 1:
			return [chunks[0], chunks[0], chunks[0], chunks[0]];
		case 2:
			return [chunks[0], chunks[1], chunks[0], chunks[1]];
		case 3:
			return [chunks[0], chunks[1], chunks[2], chunks[1]];
		case 4:
			return chunks;
	}
	
	return null;
}

/**
 * Возвращает цвет фона элемента
 * @type {Function}
 * @param {Element} elem Элемент, для которого нужно достать цвет фона
 * @param {Boolean} use_shape Для элемента создаются уголки в виде формы
 * @return {Array} Массив из 4 элементов фона
 */
var getBg = (function() {

	var session_elems = [], 
		default_color = '#ffffff';
	
	/**
	 * Основной цикл с использованием кэширования
	 */
	function mainLoopCache(elem) {
		var c;
		do {
			if (elem.nodeType != 1)
				break;
			
			if (elem.rocon_bg) { // цвет был найден ранее
				return elem.rocon_bg;
			} else { // цвет еще не найден
				session_elems.push(elem);
				c = getStyle(elem, 'background-color');
				if (c != 'transparent')
					return convertColorToHex(c);
			}
				
		} while (elem = elem.parentNode);
		
		return default_color;
	}
	
	/**
	 * Основной цикл без кэширования
	 */
	function mainLoopNoCache(elem) {
		var c;
		do {
			if (elem.nodeType != 1)
				break;
				
			c = getStyle(elem, 'background-color');
			if (c != 'transparent')
				return convertColorToHex(c);
				
		} while (elem = elem.parentNode);
		
		return default_color;
	}
	
	return function(elem, use_shape){
		var cl = /* String */elem.className, 
			bg = null;
		
		// сначала посмотрим, указан ли фон в классе элемента
		var bg_props = /\brcbg([a-f0-9_]+)\b/i.exec(cl);
		
		if (bg_props) {
			
			bg =  mapArray(expandProperty(bg_props[1]), function(el){
				return convertColorToHex('#' + el);
			});
			
			return bg;
		}
		
		// Теперь проверяем, есть ли привязанный через rocon.bindBg() к классу фон
		var elem_props = getBindedProperties(elem);
		if (elem_props) {
			return elem_props.bg;
		}
		
		if (!use_shape) 
			elem = elem.parentNode;
		
		if (getBg.use_cache) {
			session_elems = [];
			bg = mainLoopCache(elem);
			// закэшируем цвет фона у всех элементов, по которым проходились
			walkArray(session_elems, function(){
				this.rocon_bg = bg;
				getBg.processed_elems.push(this);
			});
			
			session_elems = null;
		} else {
			bg = mainLoopNoCache(elem);
		}
		
		return expandProperty(bg);
	}
})();

getBg.use_cache = true;
getBg.processed_elems = [];

function getBindedProperties(elem) {
	var cl = elem.className, result = null;
	walkArray(binded_props, function(){
		if (
			// проверка наличия подстроки
			(typeof(this.rule) == 'string' && cl.indexOf(this.rule) != -1) ||
			// проверка по регулярке
			cl.search(this.rule) != -1
		) {
			result = this;
			return false;
		}
	}, true);
	
	return result;
}

/**
 * Добавляет CSS-правило в стиль
 * @param {String} selector CSS-селектор, для которого нужно добавить правила
 * @param {String} rules CSS-правила
 */
function addRule(selector, rules) {
	corners_ss.insertRule(selector + ' {' + rules + '}', corners_ss.cssRules.length);
}

/**
 * Функция поиска правил для скругленных уголков
 * @param {Function} addFunc Функция добавления уголков
 */
function findRules(addFunc) {
	/** @type {String[]}  */
	var match;
	
	walkArray(document.styleSheets, function(){
		walkArray(this.cssRules || this.rules, function(){
			if (match = re_rule.exec(this.selectorText))
				addFunc(this, parseInt(match[1], 10));
		});
	});
}

/**
 * Очищает элемент от предыдущих вставок скругленных уголков
 * @param {Element} elem
 * @param {String} [add_class] Классы, которые нужно добавить
 * @return {Element} Переданный элемент
 */
function cleanUp(elem, add_class) {
	var	cl = (elem.className || '').replace(new RegExp('\\s*' + base_class + '[\-_].+?\\b', 'ig'), '');
	if (add_class) {
		cl += ' ' + add_class;
	}
	
	elem.className = cl;
	return elem;
}

	
/**
 * Функция добавления правил для скругленных уголков
 */
function addRoundedProperties(/* CSSStyleRule */ rule, /* Number */ radius) {
	elem_classes.push(rule.selectorText.substr(1));
}

/**
 * Создает новую таблицу стилей на странице, куда будут добавляться правила
 * для описания скругленных уголков
 * @return {CSSStyleSheet}
 */
function createStylesheet() {
	if (!corners_ss) {
		if (document.createStyleSheet) {
			corners_ss = document.createStyleSheet();
		} else {
			var style = createElement('style');
			style.rel = 'rocon';
			document.getElementsByTagName('head')[0].appendChild(style);
			
			/*
			 * Просто получить самый последний стиль не получится: иногда стили
			 * добавляются внутрь <body> (так делает счетчик Яндекса, например),
			 * в этом случае мы не можем быть уверены, что только что 
			 * добавленная таблица стилей — последняя. Поэтому пробегаетмся 
			 * по всем таблицам в поисках нашей  
			 */ 
			walkArray(document.styleSheets, function(){
				if (this.ownerNode.rel == 'rocon') {
					corners_ss = this;
					return false;
				}
			});
		}
	}
	
	return corners_ss;
}

/**
 * Возвращает массив элементов, которым нужно добавить скругленные уголки.
 * Элементом массива является объект со свойствами <code>node</code> 
 * и <code>radius</code>
 * @param {Element} [context] Откуда брать элементы
 * @return {Array}
 */
function getElementsToProcess(context) {
	var elems = [], m;
	
	walkArray((context || document).getElementsByTagName('*'), function(){
		if (m = re_class.exec(this.className || '')) {
			elems.push({node: this, radius: parseInt(m[1], 10)});
		}
	});
	
	return elems;
}

/**
 * Обрабатывает все элементы на странице, которым нужно добавить скругленные
 * уголки
 */
function processRoundedElements(context){
	var elems = getElementsToProcess(context);
	if (elems.length) {
		createStylesheet();
		walkArray(elems, function(){
			addCorners(this.node, this.radius);
		});
	}
}
/**
 * Проверяет, был ли добавлен CSS-selector в таблицу стилей
 * @param {String} selector
 * @return {Boolean}
 */
function isProcessed(selector) {
	return processed_rules[selector] ? true : false;
}

/**
 * Возвращает параметры уголка элемента
 * @param {Element} elem Элемент, у которого нужно получить параметры уголка
 * @param {Number} [radius] Радиус скругления
 */
function getCornerParams(elem, radius) {
	var cl = elem.className || '';
	radius = radius || parseInt(cl.match(re_class)[1], 10);
	var use_shape = re_shape_flag.test(cl),
		props = getBindedProperties(elem);
	
	var border_color = '';
	var border_width = props ? props.border_width : (parseInt(getStyle(elem, 'border-left-width')) || 0);
	if (border_width) {
		// нужно отрисовать бордюр
		border_color = convertColorToHex(getStyle(elem, 'border-left-color') || '#000');
	}
	
	return {
		'radius': radius,
		'bg_color': getBg(elem, use_shape),
		
		// толщина бордюра не может быть больше радиуса скругления
		// (так по CSS3 спецификации)
		'border_width': (border_width > radius) ? radius : border_width,
		'real_border_width': border_width,
		'border_color': border_color,
		'use_shape': use_shape
	};
}

/**
 * Применяет уголки к элементам, переданным в массиве. В основном вызывается из
 * <code>rocon.update()</code>
 * @param {arguments} args Аргументы функции
 * @param {Function} fn Функция, которую нужно выполнить на каждом элементе
 */
function applyCornersToArgs(args, fn) {
	walkArray(args, function(){
		walkArray((this instanceof Array) ? this : [this], fn);
	});
}

/**
 * Делает копию объекта
 * @param {Object} obj
 * @return {Object}
 */
function copyObj(obj) {
	var result = {};
	for (var p in obj) 
		if (obj.hasOwnProperty(p))
			result[p] = obj[p];
	
	return result;
}

/**
 * Корректирует CSS-свойства элемента для правильного рисования уголков в виде
 * формы
 * @param {HTMLElement} elem Элемент, который нужно подкорректировать
 * @param {String} class_name Имя создаваемого класса
 * @param {getCornerParams()} options параметры рисования уголка
 */
function adjustBox(elem, class_name, options) {
	var elem_styles = getStyle(elem, ['padding-top', 'padding-bottom', 'margin-top', 'margin-bottom']);
	function getProp(prop) {
		return parseInt(elem_styles[prop], 10) || 0;
	}
	
	/*
	 * Используем форму, поэтому у блока снижаем верхние и нижние
	 * бордюры, а также на величину радиуса снижаем верхний 
	 * и нижний паддинг 
	 */
	
	var padding_top = Math.max(getProp('paddingTop') - options.radius + options.border_width, 0),
		padding_bottom = Math.max(getProp('paddingBottom') - options.radius + options.border_width, 0),
		margin_top = getProp('marginTop') + options.radius,
		margin_bottom = getProp('marginBottom') + options.radius,
		border_width = options.real_border_width - options.border_width;
	
	addRule('.' + class_name, 
			'border-top-width:' + border_width + 'px;' +
			'border-bottom-width:' + border_width + 'px;' +
			'padding-top:' + padding_top + 'px;' +
			'padding-bottom:' + padding_bottom + 'px;' +
			'margin-top:' + margin_top + 'px;' +
			'margin-bottom:' + margin_bottom + 'px' );
}

addDomReady(processRoundedElements);
// после того, как добавили уголки, необходимо очистить кэш фона,
// иначе будут проблемы с динамическим обновлением блоков
addDomReady(function(){
	walkArray(getBg.processed_elems, function(){
		this.removeAttribute('rocon_bg');
	});
	getBg.use_cache = false;
});

bindReady();